package org.glvnsjc.webapp.listener;

import java.util.LinkedHashSet;
import java.util.Set;

import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.http.HttpSessionAttributeListener;
import javax.servlet.http.HttpSessionBindingEvent;

import org.glvnsjc.model.domain.User;
import org.springframework.security.Authentication;
import org.springframework.security.AuthenticationTrustResolver;
import org.springframework.security.AuthenticationTrustResolverImpl;
import org.springframework.security.context.HttpSessionContextIntegrationFilter;
import org.springframework.security.context.SecurityContext;
import org.springframework.security.context.SecurityContextHolder;

/**
 * UserCounterListener class used to count the current number
 * of active users for the applications.  Does this by counting
 * how many user objects are stuffed into the session.  It also grabs
 * these users and exposes them in the servlet context.
 *
 * @author <a href="mailto:matt@raibledesigns.com">Matt Raible</a>
 */
public class UserCounterListener
    implements ServletContextListener, HttpSessionAttributeListener
{
    /**
     * Name of user counter variable
     */
    public static final String COUNT_KEY = "userCounter";

    /**
     * Name of users Set in the ServletContext
     */
    public static final String USERS_KEY = "userNames";

    /**
     * The default event we're looking to trap.
     */
    public static final String EVENT_KEY = HttpSessionContextIntegrationFilter.SPRING_SECURITY_CONTEXT_KEY;

    private transient ServletContext servletContext;

    private int counter;

    private Set<User> users;

    /**
     * Initialize the context
     * @param sce the event
     */
    public synchronized void contextInitialized( ServletContextEvent sce )
    {
        servletContext = sce.getServletContext();
        servletContext.setAttribute( ( COUNT_KEY ), Integer.toString( counter ) );
    }

    /**
     * Set the servletContext, users and counter to null
     * @param event The servletContextEvent
     */
    public synchronized void contextDestroyed( ServletContextEvent event )
    {
        servletContext = null;
        users = null;
        counter = 0;
    }

    synchronized void incrementUserCounter()
    {
        counter = Integer.parseInt( (String) servletContext.getAttribute( COUNT_KEY ) );
        counter++;
        servletContext.setAttribute( COUNT_KEY, Integer.toString( counter ) );
    }

    synchronized void decrementUserCounter()
    {
        int counter = Integer.parseInt( (String) servletContext.getAttribute( COUNT_KEY ) );
        counter--;

        if ( counter < 0 )
        {
            counter = 0;
        }

        servletContext.setAttribute( COUNT_KEY, Integer.toString( counter ) );
    }

    @SuppressWarnings("unchecked")
    synchronized void addUsername( User user )
    {
        users = (Set) servletContext.getAttribute( USERS_KEY );

        if ( users == null )
        {
            users = new LinkedHashSet<User>();
        }

        if ( !users.contains( user ) )
        {
            users.add( user );
            servletContext.setAttribute( USERS_KEY, users );
            incrementUserCounter();
        }
    }

    synchronized void removeUsername( User user )
    {
        users = (Set) servletContext.getAttribute( USERS_KEY );

        if ( users != null )
        {
            users.remove( user );
        }

        servletContext.setAttribute( USERS_KEY, users );
        decrementUserCounter();
    }

    /**
     * This method is designed to catch when user's login and record their name
     * @see javax.servlet.http.HttpSessionAttributeListener#attributeAdded(javax.servlet.http.HttpSessionBindingEvent)
     * @param event the event to process
     */
    public void attributeAdded( HttpSessionBindingEvent event )
    {
        if ( event.getName().equals( EVENT_KEY ) && !isAnonymous() )
        {
            SecurityContext securityContext = (SecurityContext) event.getValue();
            if ( securityContext.getAuthentication().getPrincipal() instanceof User )
            {
                User user = (User) securityContext.getAuthentication().getPrincipal();
                addUsername( user );
            }
        }
    }

    private boolean isAnonymous()
    {
        AuthenticationTrustResolver resolver = new AuthenticationTrustResolverImpl();
        SecurityContext ctx = SecurityContextHolder.getContext();
        if ( ctx != null )
        {
            Authentication auth = ctx.getAuthentication();
            return resolver.isAnonymous( auth );
        }
        return true;
    }

    /**
     * When user's logout, remove their name from the hashMap
     * @see javax.servlet.http.HttpSessionAttributeListener#attributeRemoved(javax.servlet.http.HttpSessionBindingEvent)
     * @param event the session binding event
     */
    public void attributeRemoved( HttpSessionBindingEvent event )
    {
        if ( event.getName().equals( EVENT_KEY ) && !isAnonymous() )
        {
            SecurityContext securityContext = (SecurityContext) event.getValue();
            Authentication auth = securityContext.getAuthentication();
            if ( auth != null && ( auth.getPrincipal() instanceof User ) )
            {
                User user = (User) auth.getPrincipal();
                removeUsername( user );
            }
        }
    }

    /**
     * Needed for Acegi Security 1.0, as it adds an anonymous user to the session and
     * then replaces it after authentication. http://forum.springframework.org/showthread.php?p=63593
     * @see javax.servlet.http.HttpSessionAttributeListener#attributeReplaced(javax.servlet.http.HttpSessionBindingEvent)
     * @param event the session binding event
     */
    public void attributeReplaced( HttpSessionBindingEvent event )
    {
        if ( event.getName().equals( EVENT_KEY ) && !isAnonymous() )
        {
            SecurityContext securityContext = (SecurityContext) event.getValue();
            if ( securityContext.getAuthentication() != null )
            {
                if ( securityContext.getAuthentication().getPrincipal() instanceof User )
                {
                    User user = (User) securityContext.getAuthentication().getPrincipal();
                    addUsername( user );
                }
            }
        }
    }
}
